﻿/* 
 * BtCommLib - the OpenWebNet .NET Library
 *
 * Copyright (C) 2011 BTicino S.p.A.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Timers;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using BTicino.BtCommLib.Trace;
using System.Text.RegularExpressions;
using BTicino.BtCommLib.Exceptions;


namespace BTicino.BtCommLib.Communication
{

    class TaskContext
    {

        #region Public Get / Set Method

        public const string BT_IP_NOT_IN_RANGE_ERROR = "BT_CONN_ERR";

        public string   ctx_name {get; set;}
        public string   in_value {get; set;}
        public bool     in_exactMD { set; get; }
        public string   out_value {get; set;}
        public bool     out_result { set; get; }
        public string   out_errInd { set; get; }

        #endregion
        #region Constructor

        /// <summary>
        /// Empty contructor 
        /// </summary>
        public TaskContext()
        {
            ctx_name = null;
            in_value = null;
            in_exactMD = false;
            out_value = null;
            out_result = false;
            out_errInd = null;
        }

        #endregion 
    }


    class BTOpenFrmReaderTask
    {

        #region Private Data Member
        
        private TaskContext     m_ctx = null;
        private NetworkStream   m_ntwStr = null;
        private BTOpenLink      m_openLink = null;
        private const int       FrmBufSize = 100;
        private byte[]          m_frmBuf= new byte[FrmBufSize];
        private StringBuilder   m_frmBld = new StringBuilder();

        // Class logger
        private static BTLibLogger logger = BTLibLogger.createLogger("BTOpenFrmReaderTask");

        #endregion

        #region Constructor
        
        /// <summary>
        /// Public BTOpenFrmReaderTask Constructor
        /// </summary>
        /// <param name="theStream">TCP client</param>
        /// <param name="theLink">BTOpenLink object, theLink contain channel information</param>
        public BTOpenFrmReaderTask(NetworkStream theStream, BTOpenLink theLink)
        {
            m_ntwStr    = theStream;
            m_openLink  = theLink;
        }

        #endregion

        #region Public Method
        /// <summary>
        /// Read frames from BTicino channel
        /// </summary>
        /// <returns>Return frames received</returns>
        private string getAFrame()
        {
            string  out_val=null;
            bool    frameIsReady=false;
            int     byCnt=0;

            logger.Debug("getAFrame service invoked on Thread ..."+Thread.CurrentThread.Name);
            try
            {
                do
                {
                    byCnt = m_ntwStr.Read(m_frmBuf, 0, 1);
                    if (byCnt > 0)
                    {
                        m_frmBld.AppendFormat("{0}", Encoding.ASCII.GetString(m_frmBuf, 0, byCnt));

                        out_val = m_frmBld.ToString();
                        if (out_val.EndsWith("##"))
                        {
                            m_frmBld = new StringBuilder();
                            frameIsReady = true;
                            logger.Debug("getAFrame frame available: " + out_val);
                        }
                    }
                } while (!frameIsReady);
            }
            catch (IOException exc)
            {
                logger.Debug("getAFrame ERROR_IOExc: " + exc);
                out_val = null;
            }
            catch (ObjectDisposedException exc)
            {
                logger.Debug("getAFrame ERROR_ObjDispE: " + exc);
                out_val = null;
            }

            return out_val;
        }


        /// <summary>
        /// Read Frames from BTicino channel and save into internal data member, of TaskContext type
        /// </summary>
        /// <param name="data">TaskContext object, to save Bticino open frame read</param>
        public void readAFrame(Object data)
        {
            m_ctx = (TaskContext)data;

            logger.Debug("readAFrame service invoked ...");
            try
            {
                m_ctx.out_value= getAFrame();
                if (m_ctx.out_value != null)
                    m_ctx.out_result = true;
                else
                    m_ctx.out_result = false;
            }
            catch (ThreadAbortException exc)
            {
                logger.Debug("readAFrame thread aborted ..." + exc);
                m_ctx.out_result = false;
                m_ctx.out_value = null;
                throw (exc);
            }
            finally
            {
                logger.Debug("readAFrame service ended");
            }
        }

        /// <summary>
        /// Method use, in a separate thread, to check every 0.5 seconds, if same frame are present on
        /// BTicino channels
        /// </summary>
        /// <param name="data">null</param>
        public void frameReadLoop(Object data)
        {
            string theFrame = null;

            logger.Debug("Start frameRead Loop ...");
            

            for (;;)
            {
               
                // retrieve frmae from BTicino devices and notify its to the controller
                theFrame = getAFrame();
                m_openLink.notifyFrame(theFrame);
                
                //  Exit read loop since got an error reading a frame
                if (theFrame == null)
                    break;

                Thread.Sleep(500);
            }
        }

        /// <summary>
        /// waitng for a specific frame (eg. ACK or NACK) 
        /// </summary>
        /// <param name="data">Object that contain frame to be compare and pushing the result</param>
        public void waitAFrame(Object data)  {
            m_ctx = (TaskContext)data;
            string theFrame = null;
            bool exitLoop = false;

            logger.Debug("waitAFrame service invoked ..."+m_ctx.ctx_name);

            try
            {
                m_ctx.out_value = null;
                m_ctx.out_result = false;
                
                do
                {
                    theFrame = getAFrame();

                    if (theFrame != null)
                    {
                        logger.Debug("Waiting the frame: " + m_ctx.in_value + "  Got the frame: " + theFrame);

                        //  Collect the frames that comes before the expected one
                        m_ctx.out_value = m_ctx.out_value + "|" + theFrame;
                       
                        if (m_ctx.in_value.Contains(theFrame))
                        {
                            exitLoop = true;
                            m_ctx.out_result = true;
                        }
                        else if (m_ctx.in_exactMD == true)
                        {
                            exitLoop = true;
                            m_ctx.out_result = false;
                        }
                    }
                    else
                    {
                        logger.Debug("waitAFrame reading error ");
                        exitLoop = true;
                    }
                } while (!exitLoop);

                logger.Debug("waitAFrame ended Frame received: " + m_ctx.out_value);

            }
            catch (ThreadAbortException exc)
            {
                logger.Debug("Thread Aborted: " + exc);
                m_ctx.out_result = false;
            }
            finally
            {
                logger.Debug("waitAFrame service ended");
            }
        }

        #endregion
    }


    class BTOpenLink
    {
        #region Delegate
        
        /// <summary>
        /// Delegate to comunicate hte BTicino open frame received
        /// </summary>
        /// <param name="openRawFrame">Bticino open frame</param>
        public delegate void monFrameEveDlg(object openRawFrame);

        /// <summary>
        /// Delegate to comunicate BTicino channel down status
        /// </summary>
        public delegate void monChanDownEveDlg();

        #endregion
        #region Public Event

        /// <summary>
        /// Even to comunicate BTicino channel down status
        /// </summary>
        public event monChanDownEveDlg monChanDownEve;

        /// <summary>
        /// Event to comunicate hte BTicino open frame received
        /// </summary>
        public event monFrameEveDlg     monFrameEve;

        #endregion

        #region Data Member

        public enum ConnMode        { CMD = 0, MON = 1, CMD_ASY };
        private enum WaitResult     { KO = 0, OK = 1, T_OUT = 3 };
        
        private TcpClient           m_openCli=null;
        private string              m_chanFrame;
        private string              m_theIP=null;
        private int                 m_thePORT=0;
        private BTOpenLink.ConnMode m_chanMode;
        private Thread              m_wrkThread=null;
        private Thread              m_readerThread=null;
        private bool                m_conSts=false;
        private bool                m_firstCon = false;

        // Class logger
        private static BTLibLogger logger = BTLibLogger.createLogger("BTOpenLink");

        #endregion

        #region Contructor
        
        /// <summary>
        /// Empty constructor
        /// </summary>
        public BTOpenLink()
        {
            m_wrkThread = null;
            m_firstCon = false;
        }

        #endregion
        #region Public Method

        /// <summary>
        /// Called by frameReadLoop method, if theFrame is different from null, 
        /// forward the frame to every monFrameEve envet controller else 
        /// send a channel down event, to evary monChanDownEve event controller
        /// </summary>
        /// <param name="theFrame">Monitor frame receive from BTicino devices</param>
        public void notifyFrame(string theFrame)  {
            if (theFrame == null)
            {
                logger.Debug(string.Format("Channel: {0} is DOWN !", m_chanMode));
                m_conSts = false;
                monChanDownEve();
            }
            else
            {
                logger.Debug(string.Format("Received Frame: {0} on Channel: {1}", theFrame, m_chanMode));

                if (m_chanMode == ConnMode.MON)
                    monFrameEve(theFrame);
            }
        }
        
        /// <summary>
        /// Try to connect to BTicino device
        /// </summary>
        /// <param name="theMode">Type of connection, take from BTOpenLink.ConnMode enumeraion</param>
        /// <param name="theIp">BTicino device Ip address</param>
        /// <param name="thePort">BTicino device port to be connected</param>
        /// <returns>return TRUE if it's Ok, FALSE otherwise</returns>
        public bool connect(ConnMode theMode, string theIp, int thePort)    {

            logger.Info(string.Format("Start connection procedure for chan: {0}, on Gateway: {1}, Port: {2} ", theMode, theIp, thePort));

            bool retCode=false;
            IPAddress theIpAdd;
            m_chanMode = theMode;
            m_openCli = new TcpClient();

            try
            {
                if (IPAddress.TryParse(theIp, out theIpAdd))
                {
                    m_openCli.Connect(theIp, thePort);

                    if (doConnect())
                    {
                        retCode = true;
                        m_conSts = true;
                        m_theIP = theIp;
                        m_thePORT = thePort;
                        m_firstCon = true;
                        if (m_chanMode == ConnMode.MON)
                            startReadLoop();
                    }
                    else
                    {
                        // Trace the error: connection flow error
                        retCode = false;
                    }

                }
                else
                {
                    // TRC IPAddr worng format
                    retCode = false;
                }

                if (retCode == true)
                    logger.Info(string.Format("Connection procedure for chan: {0}, on Gateway: {1}, Port: {2} Ended with: {3}", theMode, theIp, thePort, "Success !"));
                else
                    logger.Error(string.Format("Connection procedure for chan: {0}, on Gateway: {1}, Port: {2} Ended with: {3}", theMode, theIp, thePort, "ERROR !!"));
            }
            catch (SystemException ex)
            {
                logger.Error(string.Format("Connection procedure for chan: {0}, on Gateway: {1}, Port: {2} Ended with: {3}", theMode, theIp, thePort, "ERROR !!"));
                logger.Debug("Connect error exception details: " + ex);
                retCode = false;
            }
            catch (LibException ex)
            {
                logger.Error(string.Format("Connection procedure for chan: {0}, on Gateway: {1}, Port: {2} Ended with: {3}", theMode, theIp, thePort, "ERROR, IP not in range !!"));
                retCode = false;
                throw ex; 
            }

            return(retCode);
        }

        /// <summary>
        /// Diconnect every devices, channel and destroy active thread
        /// </summary>
        public void disconnect()    {
  
            logger.Info("Disconnecting from Gateway: "+m_theIP+"  Port: "+m_thePORT);


            if (m_wrkThread != null)
                m_wrkThread.Abort();

            if (m_readerThread != null)
                m_readerThread.Abort();

            if (m_openCli != null)
                m_openCli.Close();

            m_openCli = null;
            m_wrkThread = null;
            m_readerThread = null;

            m_conSts = false;

        }

        /// <summary>
        /// Check status channel, if ok send frame to low level and if necessary wait the answare
        /// If channel is down, it proceedes to call disconnect() method, after reconnect channel and try again
        /// </summary>
        /// <param name="theFrame">Frame to be sent</param>
        /// <param name="replayFrames">Frame in answare</param>
        /// <param name="retryMode">TRUE if to proceed to retry, False otherwise</param>
        /// <returns>TRUE if frame is sent, FALSE otherwise</returns>
        public bool sendAFrame(string theFrame, out string replayFrames, bool retryMode)
        {
            bool retCode = false;
            string collecteFrames = null;

            replayFrames = null;

            logger.Debug("sendAFrame service invoked with frame: " + theFrame + " on chan: " + m_chanMode);

            if ((m_chanMode == ConnMode.MON) && (m_conSts == true))
                return (false);

            retCode = writeFrame(theFrame);

            if (retCode == false)
            {
                logger.Debug("Channel connection is down try to connect: " + m_chanMode);
                if ((m_conSts == false) && (m_firstCon == true))
                    if (reConnect())
                        retCode = writeFrame(theFrame);
                    else
                        retCode = false;
            }


            if (m_chanMode == ConnMode.CMD_ASY)
                retCode = true;
            else if (m_chanMode == ConnMode.CMD)
            {
                if (waitAFrame(BTOpenConstFrame.ACK_NACK_FRM, 5, false, out collecteFrames) == WaitResult.OK)
                {
                    replayFrames = collecteFrames;
                    if (replayFrames.Contains(BTOpenConstFrame.ACK_FRM))
                        retCode = true;
                    else
                        retCode = false;
                }
                else
                {
                    if (retryMode)
                        retCode = false;
                    else
                    {
                        // if not in retry-mode issue a disconnect and a resend
                        disconnect();
                        retCode = sendAFrame(theFrame, out collecteFrames, true);
                        replayFrames = collecteFrames;
                    }
                }
            }
            return (retCode);
        }

        /// <summary>
        /// Check if device is connected
        /// </summary>
        /// <returns>TRUE if device is connected, FALSE otherwise</returns>
        public bool isConnected()
        {
            return m_conSts;
        }

        #endregion
        #region Private Method

        /// <summary>
        /// Take new stream form open channel
        /// </summary>
        /// <returns>New stream object</returns>
        private NetworkStream getNetwStream()
        {
            NetworkStream theNtwStr=null;
            try
            {
                theNtwStr = m_openCli.GetStream();
            }
            catch (SystemException)
            {
                logger.Debug("Network Stream for Channel: "+m_chanMode+"  no longer available");
            }

            return (theNtwStr);
        }

        /// <summary>
        /// Makes a waiting thread to collect device answare, The thread is stoped 
        /// when find an answare or for timeout
        /// </summary>
        /// <param name="theFrame">Frame to be compared (eg. ACK, NACK, etc... )</param>
        /// <param name="timeOut">Thread timeout, write in seconds</param>
        /// <param name="exactMatch">Extra parameter</param>
        /// <param name="collectedFrames">collectedFrames contain response frame, received from the device</param>
        /// <returns>return OK if find the answare and it's equal to theFrame (input parameter)</returns>
        private WaitResult waitAFrame(string theFrame, int timeOut, bool exactMatch, out string collectedFrames)
        {
            WaitResult retCode = WaitResult.KO;

            int curTOut = timeOut * 1000;
            collectedFrames = null;

            logger.Debug("waitAFrame service invoked for frame: " +theFrame + " with TimeOut: " + timeOut + " Sec. and ExactMatchMode: " + exactMatch);

            NetworkStream theNtwStr = getNetwStream();

            if ((m_wrkThread == null) && (theNtwStr !=null) )
            {
                //  Create a background thread with the Related task
     
                m_wrkThread = new Thread(new BTOpenFrmReaderTask(m_openCli.GetStream(), this).waitAFrame);
                m_wrkThread.Name = "waitFrameTask";
                m_wrkThread.IsBackground = true;
               

                //  Sets its context and Starts the task
                TaskContext t_ctx = new TaskContext();
                t_ctx.ctx_name = "waitFrameTask:"+m_chanMode;
                t_ctx.in_value=theFrame;
                t_ctx.in_exactMD = exactMatch;

                m_wrkThread.Start(t_ctx);

                // Wait at least timeout seconds
                m_wrkThread.Join(curTOut);

                logger.Debug("waitAFrame service End, Return to original Thread, result: " + t_ctx.out_value);
                if (!m_wrkThread.IsAlive)
                {
                    m_wrkThread = null;
                    if (t_ctx.out_result)
                    {
                        logger.Debug("waitAFrame service success");
                        collectedFrames = t_ctx.out_value;
                        retCode = WaitResult.OK;
                    }
                    else
                    {
                        logger.Debug("waitAFrame service failed !!");
                        retCode = WaitResult.KO;
                        if (t_ctx.out_value != null)
                            collectedFrames = t_ctx.out_value;
                    }
                }
                else
                {
                    logger.Debug("waitAFrame service Timeout !!");
                    m_wrkThread.Abort();
                    m_wrkThread = null;
                    retCode = WaitResult.T_OUT;
                }
            }
            else
            {
                //  Operation already on-goig 
                retCode = WaitResult.KO;
            }

            
            return (retCode);
        }

        /// <summary>
        /// Send frame without waiting for the answare
        /// </summary>
        /// <param name="theFrame">Frame to be sent</param>
        /// <returns>Retunr TRUE if the frame is sent, FALSE otherwise</returns>
        private bool writeFrame(string theFrame)
        {
            bool retCode = false;

            logger.Debug("writeFrame service Invoked with Frame: " + theFrame);
            try
            {
                byte[] myWriteBuffer = Encoding.ASCII.GetBytes(theFrame);
                m_openCli.GetStream().Write(myWriteBuffer, 0, myWriteBuffer.Length);

                retCode = true;
                
            }
            catch (SystemException e)
            {
                // TRACE THE Error ...  
                logger.Error("writeFrame service error" + e);
                retCode=false;
                m_conSts = false;
            }

            return (retCode);
        }

        /// <summary>
        /// Try to reconnect monitor channel
        /// </summary>
        /// <returns>TRUE if channel is reconnected, FALSE otherwise</returns>
        private bool reConnect() {
            
            logger.Info("Reconnect procedure invoked ...");

            disconnect();

            return(connect(m_chanMode, m_theIP, m_thePORT));
        }

        /// <summary>
        /// Connection step implementation, to initilize channel protocol
        /// </summary>
        /// <returns>TRUE if if protocol is copleted correctly</returns>
        private bool doConnect()
        {
            string collecteFrames = null;
            bool retCode = false;

            logger.Debug("doConnect procedure invoked ...");

            //  Set the channel type frame depending on the operational mode
            if ((m_chanMode == BTOpenLink.ConnMode.CMD) || m_chanMode == BTOpenLink.ConnMode.CMD_ASY)
                m_chanFrame = BTOpenConstFrame.CMDCHAN_FRM;
            else
                m_chanFrame = BTOpenConstFrame.MONCHAN_FRM;

            //  Execute the foreseen negotiation
            if (waitAFrame(BTOpenConstFrame.ACK_FRM, 30, true, out collecteFrames)== WaitResult.OK)
                if (writeFrame(m_chanFrame))
                    if (waitAFrame(BTOpenConstFrame.ACK_FRM, 30, true, out collecteFrames )== WaitResult.OK)
                        retCode = true;
                    else
                    {
                        if (Regex.IsMatch(collecteFrames.ToString(), BTOpenConstFrame.NOT_IN_RANGE_FRM))
                        {
                            logger.Debug("Waiting for channel mode:" + m_chanMode + " confirmation failed, IP not in range !!");
                            throw new LibException(TaskContext.BT_IP_NOT_IN_RANGE_ERROR);
                        }
                        else
                        {
                            logger.Debug("Waiting for channel mode:" + m_chanMode + " confirmation failed !!");
                        }
                    }
                else
                    logger.Debug("Write channel mode failed !!!");
            else
                logger.Debug("Waiting for initial ACK Failed !!!");

            if (retCode == true)
                logger.Debug("doConnect procedure invoked ended with success !");

            return (retCode);
        }

        /// <summary>
        /// Create a thread to stay in loop to receive same, event, frame from devices
        /// </summary>
        private void startReadLoop()
        {
            if (m_readerThread == null)
            {
                logger.Debug("Start read Thread for chan: " + m_chanMode);

                //  Create a background thread to put in readLoop
                m_readerThread = new Thread(new BTOpenFrmReaderTask(m_openCli.GetStream(), this).frameReadLoop);
                m_readerThread.Name = "frameReadLoop";
                m_readerThread.IsBackground = true;

                m_readerThread.Start(null);
            }
        }


        /// <summary>
        /// Stop the thread that was waiting for events 
        /// </summary>
        private void stopReadLoop()
        {
            logger.Debug("Stop read Thread for chan: " + m_chanMode);

            if ((m_readerThread != null) && (m_readerThread.IsAlive) )
                m_readerThread.Abort();
            m_readerThread = null;
        }

        #endregion
    }
}
